import spidermonkey
from spidermonkey import Runtime
from sgmllib import SGMLParser, SGMLParseError
from client import HttpHoneyClient
import dataetc, sys, md5, getopt, types
import urlparse

import activeX

# copied from msgbus
# XXX - work around urlparse.urlsplit() < Python 2.5
def parse_netloc(scheme, netloc):
    l = netloc.split('@')
    if len(l) == 2:
        username, password = l.pop(0).split(':')
    else:
        username = password = None
    l = l[0].split(':')
    if len(l) == 2:
        hostname = l[0]
        port = int(l[1])
    else:
        hostname = l[0]
        port = scheme == 'https' and 443 or 80
    return username, password, hostname, port

class Array(object): # like list in python, but there is an attr 'length'. used to simulate js array.
    def __init__(self, length = 0):
        assert length>=0
        self.length = length

    def __getitem__(self, key):
        if key in self.__dict__: return self.__dict__[key]
        return None

    def __setitem__(self, key, value):
        self.__dict__[key] = value
        if isinstance(key, int) and key>=self.length: self.length = key+1

    def append(self, obj):
        self[self.length]=obj

    def remove(self, obj):
        for key in self.__dict__:
            if self.__dict__[key] == obj:
                del self.__dict__[key]
                if isinstance(key, int) and key >= 0:
                    for k in self.__dict__:
                        if isinstance(k, int) and k>key:
                            self.__dict__[k-1]=self.__dict__[k]
                            del self.__dict__[k]
                    length -= 1
                break

class DOMObject(object):
    def __init__(self, tag, parser):
        self.tagName = tag
        self.__parser = parser
        if tag == 'body': document.body = self
        if tag == 'select': self.options = Array()
        if tag == 'input': self.value = ''
        if tag == 'option': self.value = 0
        self.children = []
        self.childNodes = Array()
        self.style = dataetc.CSSStyleDeclaration()
        document.all.append(self)

    def __setattr__(self, name, val):
        if name == 'id' or name == 'name': # when id or name is set, add the object with the id or name to js_context.
            self.__dict__[name] = val
            val = val.replace(':','_').replace('-','_')
            try:
                if cx.execute('typeof '+val+' == "undefined"'): cx.add_global(val, self)
            except: pass
            form_list[-1].__setattr__(val, self) # let this object be member of the last container (document or form).
            return
        if dataetc.isevent(name, self.tagName): #if it's an event, let it be a function
            if isinstance(val, str):
                # using 'this' in methods may cause additional problems.
                # i think i find a way to handle this, but there could be some cases it cannot cover.
                try:
                    if 'id' in self.__dict__:
                        cx.execute(self.id+'.'+name+'=function(){'+val+'}')
                    else: self.__dict__[name] = cx.execute('function(){'+val+'}')
                except: pass
            else: self.__dict__[name] = val
            return
        if name == 'innerHTML': # handling of innerHTML. note that using 'begin' and 'end' may cause confliction, but we have no other choices.
            try:
                self.__parser.html = self.__parser.html[:self.begin] + val + self.__parser.html[self.end:]
                dev = self.end - self.begin - 1 - len(val)
                for i in document.all:
                    if i.begin:
                        if i.begin > self.end: i.begin -= dev
                    if i.end:
                        if i.end > self.end: i.end -= dev
                self.__parser.current -= dev
            except Exception, e: print e
            self.parser = PageParser(self, val)
            return

        self.__dict__[name]=val

    def focus(self):
        if 'onfocus' in self.__dict__: self.onfocus()
        document.activeElement = self

    def blur(self):
        if 'onblur' in self.__dict__: self.onblur()

    def __getattr__(self, name):
        if name == 'innerHTML': return self.__parser.html[self.begin:self.end]
        return unknown()

    def appendChild(self, dom):
        if self.childNodes.length == 0: self.firstChild = dom
        self.childNodes.append(dom)
        self.lastChild = dom
        dom.__dict__['parentNode'] = self
        self.children.append(dom)

    def removeChild(self, dom):
        self.childNodes.remove(dom)
        if self.childNodes.length == 0:
            self.firstChild = None
            self.lastChild = None
        else:
            self.firstChild = self.childNodes[0]
            self.lastChild = self.childNodes[self.childNodes.length-1]

    def setAttribute(self, attname, attval):
        self.__setattr__(attname, attval)

    def removeAttribute(self, attname):
        if self.__dict__[attname]: del self.__dict__[attname]

class Window(object):
    def __init__(self, url):
        self.__dict__['__rt']=Runtime()
        self.__dict__['__cx']=self.__dict__['__rt'].new_context()
        document = Document()
        self.__dict__['__cx'].add_global('window', self)
        self.__dict__['__cx'].add_global('document', document)
        hc = HttpHoneyClient()
        scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
        self.__dict__['__html'] = ''
        try: # if url starts with 'file://' read from file, else download from internet
            if scheme.lower() in ('file', ):
                f=file(url[7:],'r')
                self.__dict__['__html'] = f.read()
                f.close()
            else:
                self.__dict__['__html'] = hc.get(url)
        except Exception, e: print e
        document.location = url
        self.__dict__['__cx'].add_global('location', document.location)
        add_global_func(self.__dict__['__cx'])
        self.__dict__['__sl'] = []
        self.__dict__['__fl'] = [document]
        self.__call__()

    def __setattr__(self, name, val): # cause window.xxx is the same as xxx, so when window.xxx is set, bind xxx to js_context
        if not name.startswith('__'):
            self.__dict__['__cx'].add_global(name, val)
        self.__dict__[name]=val

    def __getattribute__(self, name): # when visit window.xxx, try get xxx from js_context
        if name.startswith('__'): return object.__getattribute__(self, name)
        if self.__dict__['__cx'].execute('typeof '+name+' == "undefined"'): return None
        else: return self.__dict__['__cx'].execute(name)

    def __call__(self): # use to active a window object
        global window
        global document
        global rt
        global cx
        global html
        global script_list
        global form_list
        window = self
        document = self.document
        rt = self.__dict__['__rt']
        cx = self.__dict__['__cx']
        html = self.__dict__['__html']
        script_list = self.__dict__['__sl']
        form_list = self.__dict__['__fl']

class Location(object):
    def __init__(self, url):
        self.__dict__['href'] = url

    def toString(self):
        return self.href

    def __setattr__(self, name, val):
        if name == 'href':
            self.__dict__['href']=self.fix_url(val)
            wind = Window(self.__dict__['href'])
            parser = PageParser(document, html)
            parser.close()

    def __getattr__(self, name):
        scheme, netloc, path, query, fragment = urlparse.urlsplit(self.__dict__['href'])
        username, password, hostname, port = parse_netloc(scheme, netloc)
        if name == 'protocol': return scheme
        if name == 'host': return netloc
        if name == 'hostname': return hostname
        if name == 'port': return port
        if name == 'pathname': return path
        if name == 'search': return query
        return None

    def fix_url(self, url):
        # fix up relative URLs to absolute URLs
        scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
        if scheme not in ('http', 'https'):
            if url.startswith('/'): 
                url = '%s//%s%s' % (self.protocol, self.host, url)
            elif url.startswith('?'): 
                url = '%s//%s%s%s' % (self.protocol, self.host, self.pathname, url)
            else: url = '/'.join(self.href.split('/')[:-1]) + '/' + url
        return url

class Document(DOMObject):
    def __init__(self):
        self.tagName = 'document'
        self.cookie = ''
        self.children=[]
        self.childNodes = Array()
        self.activeElement = None
        self.__dict__['all']=[]

    def __setattr__(self, name, val):
        if name == 'location':
            self.__dict__['location'] = Location(val)
            return
        if name == 'all': return
        self.__dict__[name]=val

    def createElement(self, tag):
        DOMObject(tag, None)
        return self.all[-1]

    def getElementById(self, ID):
        for p in self.all:
            if p.id == ID: return p

    def getElementsByName(self, name):
        self.tempArray=[]
        for p in self.all:
            if p.name == name: self.tempArray.append(p)
        return self.tempArray

    def getElementsByTagName(self, tag):
        self.tempArray=[]
        for p in self.all:
            if p.tagName == tag: self.tempArray.append(p)
        return self.tempArray

    def write(self, html):
        if 'parser' not in script_list[-1].__dict__:
            # each 'script' object has its own parser in order to deal with 'document.write'. feed the argument to the parser.
            script_list[-1].parser = PageParser(script_list[-1], html)
        else: script_list[-1].parser.feed(html)

    def writeln(self, html):
        self.write(html+'\n')

class unknown(object):
    # this class is used to simulate the member of DOM object (especially 'document') which we haven't implemented.
    # it's both unnecessary and hard to simulate all member of every DOM object.
    def __init__(self, *arg): pass

    def __getattr__(self, name):
        return unknown()

    def __getitem__(self, key):
        return unknown()

    def __call__(self, *arg):
        return unknown()

    def __setattr__(self, name, val):
        print 'attr '+name+' set to: '
        print val

    def __setitem__(self, key, val): pass

    def __noSuchMethod__(self, name, *arg): pass

class PageParser(SGMLParser):
    def __init__(self, doc, html):
        SGMLParser.__init__(self)
        self.DOM_stack = [doc] # doc is usually document, but if the parser is called by document.write, doc is the script object
        self.html = html
        self.current = 0
        self.text = ''
        self.in_Script = False
        self.script = ''
        self.endearly = False # for some cases, such as location is changed, the parser will stop early.
        self.feed(html)

    def start_object(self, attrs):
        for k, v in attrs:
            if k == 'classid': domobj = activeX.ActiveXObject(v, 'id')
            # activeX object may be initiallized by classid or classname.
            # if created by 'object' tag, classid will be used. check out the class definition in 'activeX.py' for more.
        self.DOM_stack[-1].appendChild(domobj)
        self.DOM_stack.append(domobj)
        for k, v in attrs:
            if k == 'id' or k == 'name':
                if cx.execute('typeof '+v+' == "undefined"'): cx.add_global(v, domobj)
                form_list[-1].__setattr__(v, domobj)
            domobj.__setattr__(dataetc.attrTrans(k, 'object'), v)

    def end_object(self):
        while self.DOM_stack:
            s = self.DOM_stack.pop()
            if s.tagName == 'object': break

    def start_script(self, attrs):
        self.in_Script = True
        self.literal = 1
        self.unknown_starttag('script', attrs)

    def end_script(self):
        if self.endearly: return

        self.in_Script = False
        self.literal = 0
        if 'src' in self.DOM_stack[-1].__dict__: # if the script object has attr 'src', download the source file.
            src = document.location.fix_url(self.DOM_stack[-1].src)
            hc = HttpHoneyClient()
            self.script = hc.get(src)
        global script_list
        script_list.append(self.DOM_stack[-1])
        try: cx.execute(self.script) # execute script here
        except Exception, e:
            if options['print_error']:
                print 'Error executing:\n'+self.script
                if isinstance(script_list[-1].src, str): print script_list[-1].src
            #sys.exit(0)
        try: cx.execute('')
        except: pass
        # there is a bug in py-sm, that exception cannot be handled correctly and will be
        # passed on to next 'cx.execute'. add an empty execute to balance out the effection.
        if script_list == []:
            self.endearly = True
            self.close()
            return
        scr = script_list.pop()
        if scr.parser != None: scr.parser.close()
        self.script = ''
        self.unknown_endtag('script')

    def start_iframe(self, attrs):
        global window # create a new 'window' object for this iframe.
        self.tempwin = window
        self.unknown_starttag('iframe', attrs)
        src = document.location.fix_url(self.DOM_stack[-1].src)
        if isinstance(src, str):
            window = Window(src)
            parser = PageParser(document, html)
            parser.close()

    def end_iframe(self):
        self.tempwin() # restore the original 'window' object.
        self.unknown_endtag('iframe')

    def unknown_starttag(self, tag, attrs):
        if self.endearly: return

        domobj = DOMObject(tag, self)
        #sometimes k in tag is not really attrname, so a transform is needed.
        #note that this is IE way. In firefox transform is done in DOMObject.setAttribute()
        for k, v in attrs:
            domobj.setAttribute(dataetc.attrTrans(k, tag), v)

        self.DOM_stack[-1].appendChild(domobj)
        self.DOM_stack.append(domobj)

        try: # the following is used to get the location of a tag in the html document. it's used for 'innerHTML' implementation.
            begin = self.html.lower()[self.current:].index('<'+tag)
            self.current += begin + self.html.lower()[self.current+begin:].index('>') + 1
            domobj.begin = self.current
        except: pass

        if (tag == 'form'): form_list.append(domobj)
        if (tag == 'br' or tag == 'meta'): self.unknown_endtag(tag) # <br> and <meta> have no end tag.
        if (tag == 'select'): self.lastselect = domobj
        if (tag == 'option'):
            try: self.lastselect.options.append(domobj)
            except: pass
    
    def unknown_endtag(self, tag):
        if self.endearly: return
        tempstack = []
        tempstack[0:0] = self.DOM_stack
        while self.DOM_stack:
            s = self.DOM_stack.pop()
            if s.tagName == tag: break # maybe there's some tag without end_tag. So pop until we find the match tag
        if s.tagName != tag: # if there is no corresponding start tag, ignore this end tag.
            self.DOM_stack = tempstack
            return
        try: s.onload()
        except: pass

        # also for innerHTML
        try: s.end = self.current + self.html.lower()[self.current:].index('</'+tag)
        except: pass

        if(tag == 'form'): form_list.pop()

    def unknown_startendtag(self, tag, attrs):
        # it seems that SGMLparser cannot handle startendtag such as <body/>. i have no idea when this function will be called.
        unknown_starttag(self, tag, attrs)
        unknown_endtag(self, tag)

    def characters(self, content):
        self.text = self.text + content

    def handle_data(self, text):
        if self.in_Script: self.script += text

def alert(x): print x

navigator = dataetc.Navigator()

timeout = []
def setTimeout(x, t):
    timeout.append(x)

def add_global_func(context):
    context.add_global("alert", alert)
    context.add_global("setTimeout", setTimeout)
    context.add_global("ActiveXObject", activeX.ActiveXObject)
    context.add_global("navigator", navigator)
    context.add_global("screen", unknown())
    context.execute("Event = function(){}")
    for clsname in dataetc.classlist: # bind DOM classes such as 'Image' to js_context
        exec 'class '+clsname+'(DOMObject):\n\tdef __init__(self, *arg): self.tagName="'+dataetc.classtotag(clsname)+'"'
        exec 'context.add_global("'+clsname+'",'+clsname+')'

usage = '''
usage:  -t: print the DOM tree
        -e: print scripts where error occurs
        -d: do not use detection script
'''

if __name__ == '__main__':
    #print sys.argv[1]
    try: opts, args = getopt.getopt(sys.argv[1:], 'etd')
    except: print usage

    options = {}
    options['print_tree'] = False
    options['print_error'] = False
    for o, a in opts:
        if o == '-t': options['print_tree'] = True
        elif o == '-e': options['print_error'] = True
        elif o == '-d': activeX.clear_list()
        else: print usage
    url = args[0]

    window=Window(url)

    #define and add DOM classes into js. To get a clean code, the list of class names is put into dataetc.py.

    parser = PageParser(document, html)
    parser.close()

    for i in timeout:
        try: cx.execute(i)
        except:
            try: cx.execute('if(typeof '+i+'=="function") '+i+'()')
            except: pass

    for i in document.all:
        if 'onclick' in i.__dict__:
            try: i.onclick()
            except: pass
        if 'onsubmit' in i.__dict__:
            try: i.onsubmit()
            except: pass
        if 'onmouseover' in i.__dict__:
            try: i.onmouseover()
            except: pass
        if 'onmouseout' in i.__dict__:
            try: i.onmouseout()
            except: pass

    def traverse(dom, k):
        for i in dom.children:
            for j in range(k): print '-',
            print i.tagName
            if i.tagName != 'object': traverse(i, k+1)

    if options['print_tree']: traverse(document, 0)

    activeX.write_log(md5.md5(document.location.href).hexdigest())